fix(create-app): design contracts for local Docker start failure and transient UVE 403 (#37262) - #37264
Conversation
|
Claude finished @fmontes's task in 3m 19s —— View job Code review — #37264Reviewed the current head ( Resolved
Notes (non-blocking)
No issues found. |
244a69d to
38e8ae1
Compare
4877b92 to
c3243d6
Compare
…37262) Found by running the CLI end to end (T054), which turned out to be automatable after all - every prompt has a flag, and the no-TTY path is one the port-reuse logic already handles by design, so `node index.js my-app -d <dir> -f nextjs --local` runs the whole thing unattended. The .env written for AC-004 used variable names I invented rather than the ones the project reads. It emitted DOTCMS_AUTH_TOKEN where Next.js reads NEXT_PUBLIC_DOTCMS_AUTH_TOKEN, and omitted NEXT_PUBLIC_DOTCMS_MODE entirely. The file looked entirely plausible and `npm run dev` would have failed to authenticate with nothing explaining why - the same failure shape as the bug this issue is about, a green signal that does not mean what it claims. The unit test did not catch it because it asserted the file "contains the host, token and siteId", which a wrong-named file satisfies perfectly. Two more cases were wrong for the same reason: Astro reads PUBLIC_* with a different variable set, and Angular has no dotenv file at all - it reads a TypeScript `environment` object, so writing .env there was cargo-culting. The `framework` field was already on RecoverableState and simply never used. Root cause was duplication: getEnvVariablesForNextJS had defined the real names a few hundred lines away in the same package. There is now a single owner, getEnvFileSpec(), used by BOTH the printed block and the written file, so they cannot drift apart again. It returns filename: null for frameworks with no dotenv file, which is what makes the Angular case correct rather than merely skipped. The spec now pins all three shapes, including a negative assertion that the bare DOTCMS_AUTH_TOKEN name never appears on its own. Also reworded the final steps, which told the user to `touch .env` moments before the CLI wrote that same file - instructions contradicting an action the tool was about to take. Verified by re-running the same end-to-end path: exit 0 in ~100s, project scaffolded, all four NEXT_PUBLIC_* variables present and correct, compose file back in the project directory with no orphan in the parent, and UVE configured with no 403 - which is the causal chain behaving as diagnosed, since a clean boot is exactly the case that should not 403. 128 tests, 10 suites; build and lint clean. Refs #37262 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…37262) Two defects in the wiring, both found by running the CLI against a real running instance (T054 step 5b) and neither reachable by the unit tests. Reuse could never trigger. resolvePortConflict required 8082 to be the ONLY busy port, but a running stack publishes 8082 AND 8443, so the condition could not occur in practice and reproduction step 6 stayed broken. Twelve tests passed because the fixture used a single busy port - a shape no real instance produces. The fixtures are now derived from what the bundled asset actually publishes and named A_REAL_RUNNING_STACK, since the abstraction was the bug. Reuse was then ignored anyway. The decision was computed and used only to choose a spinner message; the CLI went on to write a compose file and run `up` regardless, which failed with "Bind for 0.0.0.0:8082 failed: port is already allocated". Reuse that still provisions is not reuse. Provisioning is now skipped when reusing, which is the whole point of the decision. REQUIRED_PORTS still listed 9200 and 9600, inherited from the shared compose example the CLI no longer downloads. The bundled asset publishes neither - OpenSearch has no ports section - so the CLI refused to run for anyone with their own OpenSearch on 9200, over a conflict that cannot happen. It now checks 8082, 8443 and 8090, and a spec parses the asset and asserts the two lists agree so they cannot drift again. Verified end to end against a real instance: the CLI reuses a running stack, skips provisioning, and completes (exit 0, scaffolded, .env written). A separate run confirmed the opposite branch too - an instance that is up but cannot issue a token is correctly refused rather than adopted, with "Something is listening on 8082, but it did not answer as a usable dotCMS". Both 403 messages are now verified against real code rather than mocks, using a stub server whose UVE endpoint always 403s: local - exit 0, scaffolded, .env written; says unrecoverable, gives `docker compose down -v`, cites #37268, WITHHOLDS the manual guide. remote - exit 0, scaffolded, .env written; names the permission problem, the site and the app key, OFFERS the guide, and leaks neither `down -v` nor #37268. In both cases the UVE endpoint was hit exactly ONCE - no POST after a 403, no retry, no poll (AC-005). 132 tests, 10 suites; build and lint clean. Refs #37262 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prompt said:
? A dotCMS instance is already running on port 8082. What would you like to do?
> Reuse the running instance
Abort so I can check what it is
That states a fact and then abandons the user. It never says WHAT is running, so
there is no basis for choosing - "is that my last run, or the instance I am using
for other work?" - and the only two answers are take it or quit. Anyone who did
not want that instance had to leave the CLI and work out the docker incantation
themselves.
It now says what it found and offers a way through:
⚠ Found a dotCMS already running at http://localhost:8082
Docker project "my-app" · Up 8 minutes (healthy)
? How would you like to continue?
> Use this instance for my project
Fastest. Keeps its existing content.
Replace it with a clean instance
Stops it and DELETES its data, then starts fresh.
Cancel
Change nothing and exit.
describePortOwner() reads the compose labels off whatever publishes the port, so
the project name, health and uptime are shown before the question is asked.
Replace runs `docker compose -p <project> down -v` and then provisions fresh. The
-v is the point: keeping the volumes keeps the corruption, so a bricked instance
would come back just as broken (#37268). This turns the documented recovery from
something the CLI tells you to go and do into something it can do.
Two safety properties:
* Replace is only offered when a compose project owns the port. A container
started outside compose has no project label and is not ours to destroy, so the
option is withheld rather than offered and then failed.
* Replace is NEVER selected non-interactively. Destroying an instance is not
something to infer from the absence of a TTY; a scripted run still auto-reuses
with a printed notice, per decision D3.
Verified against a real running stack: owner detection reports
`Docker project "my-app" · Up 8 minutes (healthy)`, and a non-interactive run
still prints its notice, reuses, and completes with .env written and exit 0.
136 tests, 10 suites; build and lint clean.
Refs #37262
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prompt printed its question and nothing under it: ? How would you like to continue? It used `type: 'list'`. This package is on inquirer 13, which is built on @inquirer/prompts, where the single-choice prompt is `select` - `list` is the inquirer 8/9 name. An unregistered type renders the MESSAGE and silently renders no choices: nothing throws, nothing warns, and the user is left looking at a question with no answers. It was the only `type: 'list'` in the package. Every other prompt in asks.ts - askCloudOrLocalInstance, askFramework, prepareDirectory - already used `select`, `input`, `password` or `confirm`, and was visibly working in the same terminal session. Comparing against the neighbouring prompt that worked would have found this immediately; instead it survived a round of fixes aimed at embedded newlines and nested chalk in the choice labels, which were real smells but not the cause. Proven by holding everything else constant and changing only the type: type=list choice text rendered: 0 type=select choice text rendered: 2 A spec now guards it, and was confirmed to fail when the bug is reintroduced. It asserts asks.ts never uses type: 'list', and that every prompt type used is one this inquirer version actually registers. This is worth a test because the failure is silent and looks like a styling problem rather than a wrong prompt type, so it costs far more to diagnose than to prevent. The choice labels are also cleaned up as part of this: single-line names with the hint moved to `description`, which @inquirer/select 5.2.1 supports and renders under the highlighted option, instead of a `\n` inside the name and a nested chalk.gray inside a label inquirer re-styles when highlighting. Confirmed working in a real terminal by the reporter. 138 tests, 11 suites; build and lint clean. Refs #37262 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A successful run ended by printing the API token twice - once in a "copy this
block and paste it into your .env" section, and again in the recovery block -
while having already written that exact file itself. So it duplicated a JWT into
scrollback and told the user to redo work that was done.
Now the CLI owns the file and says so:
Wrote .env with your dotCMS connection details.
host : http://localhost:8082
site id : 48190c8c-42c4-46af-8d1a-0cd5db894797
token : stored in .env
The token is no longer echoed when it was successfully written. It is safe on
disk, and a JWT in terminal scrollback and CI logs buys nothing. The "create your
environment file" and "add your dotCMS configuration" steps are gone from the
Next.js and Astro flows, and the remaining steps renumber to 1-4: cd, npm run dev,
open the browser, edit the page.
The guarantee behind contract X1 is unchanged - no successful state is ever
discarded - but it is now satisfied by the file rather than by the terminal. When
nothing could be written the full block still prints, including the token, because
then the terminal IS the only place the run survives. Three cases take that path:
the framework has no dotenv file, a .env already exists and is left untouched, or
the write failed.
Angular deliberately keeps its paste block. It has no .env - it reads a TypeScript
environment object - so getEnvFileSpec writes nothing for it and the user really
does have to paste into the environment files. The exit handler prints the full
block for Angular for the same reason.
Also dropped `siteId` and `token` from finalStepsForNextjs and finalStepsForAstro,
which no longer render them.
140 tests, 11 suites; build and lint clean.
Refs #37262
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A successful run ended with the summary, and then a second block after it:
📋 Next Steps:
...
💬 Community: https://community.dotcms.com
Wrote .env with your dotCMS connection details.
host : ...
That was structural, not cosmetic. The exit handler runs on process 'exit', so
anything it prints necessarily lands AFTER everything else - it could only ever
append to the summary that was supposed to contain it.
flushRecoverableState() lets the success path do the env write and claim the
reporting, so the details render inside the Next Steps block, above step 1:
📋 Next Steps:
✔ Your dotCMS credentials are already in .env
host : http://localhost:8082
site id : 48190c8c-42c4-46af-8d1a-0cd5db894797
1. Navigate to your project:
...
The exit handler then stays silent, because the state has already been surfaced.
It remains the fallback for every path that never reaches the summary - the 14
process.exit sites, an unexpected throw - which is the whole point of contract X1
and is unchanged. A spec pins both halves: silent after a flush, still speaking
without one.
The same renderer covers the case where no file was written (Angular has no
dotenv file, or a .env already exists, or the write failed). It then prints the
values to paste, in the same place, rather than in a separate trailing block.
144 tests, 11 suites; build and lint clean. Placement verified in the shipped
bundle: the summary renders between the "Next Steps" heading and step 1.
Refs #37262
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Closes the gap @nicobytes raised on #37263: AC-013 is the one criterion the spec itself calls "the most likely way to break the release", and nothing actually enforced it. What existed asserted the wrong thing. packaging.spec.ts reads package.json and project.json from the SOURCE TREE, and verify-cold-start.sh --static resolves the asset relative to src/. Both pass identically whether or not the file ever ships. A wrong `output:` in the esbuild assets entry satisfies every one of them and still publishes a package with no compose file - and then every local-Docker run fails at its first step, which is precisely what AC-013 exists to prevent. The packaging had in fact been verified once, by hand, with npm pack --dry-run; there was no gate. scripts/verify-package.sh asserts the artifact instead: * the compose asset is in dist/libs/sdk/create-app at the path the CLI resolves at runtime (resolveComposeSource walks up from the bundle entry, so it must sit beside index.js exactly as it does in the source tree), and * npm pack --dry-run lists it in the tarball contents. Those are two independent failures and both are checked, because either alone ships a broken package: esbuild copies the file but `files` omits it, or `files` is right and the copy never happened. Confirmed to FAIL on both modes rather than assumed: removing dist/assets fails both checks; restoring the asset and reverting `files` to its pre-fix ["*.js", "README.md"] passes the first and fails the second, naming package.json. Wired as the nx target `verify-package` with dependsOn build, deliberately NOT folded into `test` - tests must stay fast and must not require a build. Also tags T052 with AC-011. That criterion (8090 answers on loopback, refused on the LAN address) was already enforced by verify-cold-start.sh's T007 check but referenced by no task, so the only security criterion in the list read as uncovered. Acceptance-criteria coverage is now 13/13. 144 tests, 11 suites; build, lint and both verification scripts clean. Refs #37262, #37263 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither script ran in CI. The Maven build drives nx with exactly four invocations - `nx affected -t lint`, `nx format:check`, `nx run-many -t build`, `nx affected -t test` - so `verify-package` was never reached, and verify-cold-start.sh was not an nx target at all. Both were manual, which for AC-013 in particular means the criterion the spec calls "the most likely way to break the release" had a gate nobody ran. Both now hang off this project's `test` target, so `nx affected -t test` picks them up. That keeps the change inside libs/sdk/create-app/project.json rather than editing core-web/pom.xml, which every project in the monorepo shares. New target `verify-compose-static` runs verify-cold-start.sh --static: the config-only half, no Docker, ~0.3s. It is what catches a reformatted CUSTOM_STARTER_URL line silently breaking --starter for every installed CLI. The runtime half (cold start, restart recovery, LAN exposure) needs a real daemon and a multi-minute starter import and stays manual. Both targets get `cache: true` with narrow inputs - the compose asset, the manifests and the scripts themselves - so they do not re-run on unrelated edits. Measured: 4s cold, 1s warm, against 144 Jest tests. This reverses the "keep tests fast and buildless" call made when verify-package was added; that rule earns its keep on a large Angular library, not on a CLI whose build is 1.3s. Verified the chain actually fails rather than assumed: reverting `files` in package.json to its pre-fix ["*.js", "README.md"] fails the packaging gate and aborts the run before Jest executes. Refs #37262, #37263 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
semgrep-dotcms flagged axios on this PR with several High findings, all the same root cause: axios's Node adapter does not clear `Proxy-Authorization` when a request that went through an authenticated proxy is redirected to a target that does not use that proxy, leaking the proxy credentials to the redirect origin. Bumping to axios 1.20.0 would close those CVEs. Removing axios closes the class, and the case for removing it here is unusually strong: * create-app was the ONLY consumer of axios in the workspace - no other lib or app imports it. * It sat in esbuild's `external` list, so it was a real runtime install for everyone running `npx @dotcms/create-app`, not just a build-time dependency. * compose-source.ts in this same package already used native fetch, so the inconsistency was ours. * Node >= 22.22.3 is required here (.nvmrc), where fetch is stable. New `src/utils/http.ts` is the single owner of HTTP: timeout via AbortController (fetch has none of its own, so a dead instance used to hang the CLI), best-effort JSON parsing that tolerates a 204 or a plain-text body, and an HttpError that keeps axios's `error.response.status` shape so configureUVE's statusOf() and the retry classifier keep working unchanged. Throw-on-non-2xx is the default because that is what every call site expected; `acceptAnyStatus` covers the readiness probe, where a 503 is data rather than a failure. Worth noting the fetch spec requires stripping `Authorization` on a cross-origin redirect - the protection axios's Node adapter was missing. Migrated: api/index.ts, uve/configure-uve.ts, utils/index.ts (fetchWithRetry), utils/fetch-retry.ts and index.ts. axios is gone from the published dependencies and from the esbuild external list; the shipped bundle contains zero references. The pnpm lockfile is unaffected - libs/sdk/create-app is not a separate importer, so its package.json is the published manifest rather than an install manifest. configure-uve.spec mocks the http module rather than fetch, so its cases stay about the contract (probe once, retry 5xx only, mode-dependent guidance) while the new http.spec covers the transport - 15 cases including timeout, transport failure, non-JSON bodies and the 2xx boundary. Verified over real HTTP end to end, not just against mocks: a run against a stub whose UVE endpoint 403s makes all four calls through native fetch (health, token POST, site GET, UVE probe), exits 0, scaffolds, writes .env, and prints the remote-mode 403 guidance without leaking `docker compose down -v`. 159 tests, 12 suites; build, lint and both verification gates clean. Refs #37262, #37264 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…37262) The README was updated when the compose file became bundled, then drifted: three later changes invalidated parts of it and it was never revisited. Two of those were outright wrong rather than merely stale. Wrong: * "Validates required ports: 8082, 8443, 9200, 9600". The CLI checks 8082, 8443 and 8090. The 9200/9600 pair was inherited from the shared compose example the CLI no longer downloads. * The published-ports table listed "9200, 9600 — all interfaces" for OpenSearch. The bundled stack publishes NOTHING for Postgres or OpenSearch; they are only reachable inside the compose network. Readers were being told to expect a port conflict that cannot happen, and the table now says so explicitly, since "can I run my own OpenSearch alongside this?" is the obvious question. Stale: * Both flows ended with "prints framework-specific env setup instructions". The CLI writes .env itself now. * Nothing described the port-conflict prompt, which is a destructive choice (Replace removes volumes) and needs documenting before someone meets it. * Local flow claimed a generic "waits for local health check"; it waits on /dotmgt/readyz with /api/v1/appconfiguration as fallback. * UVE configuration was presented as a required step in both flows. It is optional - failure warns and continues, which is the entire point of the fix. * Troubleshooting told the reader to go stop the process on 8082 by hand. The CLI now offers to reuse or replace it. * Requirements said "Node.js + npm"; native fetch means Node 22.22.3+ is required and the .nvmrc already pins it. * Dev commands used yarn; this workspace is pnpm. Added: "If dotCMS is already running" (the prompt, what Replace destroys, why it is withheld when no Compose project owns the port, and the non-interactive behaviour) and "Your .env" (per-framework variable names, that an existing file is never overwritten, and that Angular has none). Also documents `pnpm nx verify-package sdk-create-app`. Every remaining factual claim was cross-checked against the source rather than re-read: port list, readiness endpoints, and the DOTCMS_COMPOSE_URL escape hatch. Refs #37262 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`nx format:check` failed CI on five files this branch touched: project.json, src/api/index.ts, src/asks.spec.ts, src/exit-state.spec.ts and src/utils/ports.ts. Formatting only — no behaviour change. 159 tests, lint and both verification gates still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) `process.exit` skips `finally`, so the scaffold-failure path bypassed the very `withComposeFileMovedAside` guard that AC-008 added — it now throws, which reaches both the `finally` and the outer catch (still exit 1). The holding spot for the compose file moves to a private `mkdtemp` dir. It was the parent, i.e. the user's cwd, with `overwrite: true` — so scaffolding next to your own docker-compose.yml destroyed it, unrecoverably, since `finally` then moved ours into the project. `applyStarterUrl` uses a replacer function: as a replacement string, `$1`/`$&`/ `$'` in a `--starter` URL were expanded instead of written. Docker troubleshooting listed ports 9200/9600, which this branch dropped from REQUIRED_PORTS, and omitted 8090. Both new specs verified Red against the old code first. 166 tests (was 159). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
19fbf84 to
59890cb
Compare
wezell
left a comment
There was a problem hiding this comment.
Was waiting for dotbot to respond, but I'll approve instead
…ed (#37262) Ran the real `--local` flow with the git clone rigged to fail, which is the scaffold-failure path AC-008 is about and the one no test reached. It passes: the compose file returns to the project, a pre-existing parent docker-compose.yml stays byte-identical, and no holding dir is left behind. Three things the run found that unit tests could not: `throw created.val` rather than a new Error carrying the spinner's own text — the outer catch prints whatever it is handed, so the previous version said "Failed to scaffold frontend project (nextjs)." twice. The underlying error names the likely causes instead. `moveDockerComposeOneLevelUp` / `moveDockerComposeBack` are gone from src/git/index.ts. Dead code with no callers, not exported from the entry — but they did the unguarded parent-directory move with no `finally`, i.e. both defects review just found, one import away from returning. `getSafeErrorDetails` no longer falls back to "Axios request failed"; this package dropped axios earlier on this branch. 166 tests, lint, build and format:check clean. Stack and volumes torn down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c42dbbb
|
dotbot code review:
No P0/P1 bug introduced by this patch is provable from repo code. The new fetch client, readiness fallback, port-reuse, compose-move, and exit-state paths are consistent with callers and contracts. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · meta/muse-spark-1.3 · medium |
|
dotbot code review:
Verified against the repo: http.ts preserves the axios error shape ( Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · ~z-ai/glm-latest · medium |
dotCMS-Machine-User
left a comment
There was a problem hiding this comment.
✅ dotbot review: all reviewer models (meta/muse-spark-1.3, ~z-ai/glm-latest) agree — patch is correct.
approved automatically by dotbot
PR 2 of 2 for #37262, stacked on #37263 (the spec).
The problem
npx @dotcms/create-app --localnever starts dotCMS — it races Postgres and dies. Hand-starting corrupts the install, every UVE call 403s forever, and the CLI exits with nothing scaffolded.flowchart TD A["dotcms starts before Postgres accepts connections"] -->|no restart policy| B["exits, stays dead"] B --> C["user hand-starts it from Docker Desktop"] C --> D["starter import left incomplete"] D --> E["every Apps API call 403s — permanently"] E --> F["CLI exits 1 · empty directory · working token discarded"] FIX1["**Fix 1** — compose gates dotcms on db + opensearch<br/>healthy, with a restart policy"] FIX2["**Fix 2** — UVE failure is non-fatal:<br/>project still scaffolds, .env still written"] FIX1 -.->|removes| A FIX2 -.->|removes| F style FIX1 fill:#dff0e8,stroke:#146046,color:#000 style FIX2 fill:#dff0e8,stroke:#146046,color:#000 style F fill:#fbe7e4,stroke:#a8281f,color:#000The fix
One compose file, owned by the CLI. It ships inside the package instead of being fetched from
main, sodotcmscan be gated ondbandopensearchreporting healthy, givenrestart: unless-stopped, and health-checked on/dotmgt/livez. The sharedsingle-node-demo-siteexample is untouched. This alone removes the 403, because it removes the interrupted boot that causes it.A failed UVE call no longer destroys the run. Two duplicated exit-on-failure call sites collapse into one
configureUVE()that never callsprocess.exit. The 403 guidance differs by path: recreate the local stack, or check the token's permissions on your own server — opposite advice for the same status code.Nothing successful is discarded. An exit handler writes
.envand reports the connection details on every terminal path, including the 14process.exitsites afinallycannot reach.Recovery is unblocked. A dotCMS already on 8082 can be reused or replaced from the prompt instead of aborting; a failed
npm installnow actually reports failure; the compose file is never stranded in the parent directory.The wait is legible. Compose progress is streamed with an elapsed-time ticker, and readiness moves to
/dotmgt/readyz.Verification
144 Jest tests. Two gates now run in CI via
nx affected -t test:verify-compose-static(compose shape, no Docker) andverify-package(the asset is indist/and innpm packoutput — asserting the artifact, not the manifests).Verified end to end against a real stack: cold start healthy unaided in 60s, self-exit restarted by the policy, 8090 refused on the LAN address, and both 403 messages exercised against a stub.
Checklist
verify-cold-start.sh; the full fault-injection E2E suite belongs to e2e: Add E2E test suite for@dotcms/create-appCLI #35096127.0.0.1rather than the wildcard, and a test asserts it is refused on the LAN addressAdditional Info
No Java, no
com.dotmarketing.*, no DB/ES/REST contract change — not rollback-unsafe. Per ADR-0019 this ships in a dotCMS release rather than a standalone SDK publish.Refs #37262
This PR fixes: #37262
This PR fixes: #37262